#!/bin/sh /etc/rc.common
# Copyright 2019-2023 Stan Grishin (stangri@melmac.ca)
# shellcheck disable=SC1091,SC3043,SC3060

# shellcheck disable=SC2034
START=20
# shellcheck disable=SC2034
STOP=15
# shellcheck disable=SC2034
USE_PROCD=1

if type extra_command 1>/dev/null 2>&1; then
	extra_command 'version' 'Show version information'
else
# shellcheck disable=SC2034
	EXTRA_COMMANDS='version'
fi

readonly PKG_VERSION='dev-test'
readonly packageName='https-dns-proxy'
readonly serviceName="$packageName $PKG_VERSION"
readonly _OK_='\033[0;32m\xe2\x9c\x93\033[0m'
readonly _FAIL_='\033[0;31m\xe2\x9c\x97\033[0m'
readonly PROG=/usr/sbin/https-dns-proxy
readonly BOOTSTRAP_CF='1.1.1.1,1.0.0.1,2606:4700:4700::1111,2606:4700:4700::1001'
readonly BOOTSTRAP_GOOGLE='8.8.8.8,8.8.4.4,2001:4860:4860::8888,2001:4860:4860::8844'
readonly DEFAULT_BOOTSTRAP="${BOOTSTRAP_CF},${BOOTSTRAP_GOOGLE}"
readonly canaryDomainsMozilla='use-application-dns.net'
readonly canaryDomainsiCloud='mask.icloud.com mask-h2.icloud.com'

# Silence "Command failed: Not found" for redundant procd service delete calls
__UBUS_BIN="$(command -v ubus || echo /bin/ubus)"
ubus() {
	if [ "$1" = "call" ] && [ "$2" = "service" ] && [ "$3" = "delete" ]; then
		"$__UBUS_BIN" "$@" >/dev/null 2>&1 || true
	else
		"$__UBUS_BIN" "$@"
	fi
}

hdp_boot_flag=

# package global config variables
canary_domains_icloud=
canary_domains_mozilla=
dnsmasq_config_update=
force_dns=
force_dns_port=
force_dns_src_interface=
procd_trigger_wan6=
global_listen_addr=
global_tcp_client_limit=
global_polling_interval=
global_proxy_server=
global_force_http1=
global_force_http3=
global_force_ipv6=
global_max_idle_time=
global_conn_loss_time=
global_ca_certs_file=
global_user=
global_group=
global_verbosity=
global_logfile=
global_statistic_interval=
global_log_limit=

dnsmasq_restart() { /etc/init.d/dnsmasq restart >/dev/null 2>&1; }
is_alnum() { case "$1" in (*[![:alnum:]_\ @]*|"") return 1;; esac; }
is_fw4_restart_needed() { [ "$(uci_get "$packageName" 'config' 'force_dns' '1')" = '1' ]; }
is_mac_address() { expr "$1" : '[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]:[0-9A-F][0-9A-F]$' >/dev/null; }
is_integer() { case "$1" in ''|*[!0-9]*) return 1;; esac; [ "$1" -ge 1 ] && [ "$1" -le 65535 ] || return 1; return 0; }
is_ipv4() { expr "$1" : '[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*$' >/dev/null; }
is_ipv6() { ! is_mac_address "$1" && str_contains "$1" ":"; }
is_port_listening() {
	local hex
	is_integer "$1" || return 1
	hex="$(printf '%04X' "$1")"
	# TCP: state 0A == LISTEN
	if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h && $4=="0A") {found=1}} END{exit found?0:1}' /proc/net/tcp /proc/net/tcp6 2>/dev/null; then
		return 0
	fi
	# UDP: presence indicates a bound socket
	if awk -v h="$hex" 'NR>1{split($2,a,":"); if (toupper(a[2])==h) {found=1}} END{exit found?0:1}' /proc/net/udp /proc/net/udp6 2>/dev/null; then
		return 0
	fi
	return 1
}
is_resolver_working() { 
	local heartbeat_domain heartbeat_sleep_timeout heartbeat_wait_timeout
	config_load "$packageName"
	config_get heartbeat_domain        'config' 'heartbeat_domain'        'heartbeat.melmac.ca'
	config_get heartbeat_sleep_timeout 'config' 'heartbeat_sleep_timeout' '10'
	config_get heartbeat_wait_timeout  'config' 'heartbeat_wait_timeout'  '30'
	[ "$heartbeat_domain" = '-' ] && return 0
	is_integer "$heartbeat_sleep_timeout" && sleep "$heartbeat_sleep_timeout"
	resolveip -t "$heartbeat_wait_timeout" "$heartbeat_domain" >/dev/null 2>&1
}
output() {
	[ -z "$verbosity" ] && verbosity="$(uci_get "$packageName" 'config' 'verbosity' '1')"
	[ "$#" -ne '1' ] && {
		case "$1" in [0-9]) [ $((verbosity & $1)) -gt 0 ] && shift || return 0;; esac }
	local msg="$*" queue="/dev/shm/$packageName-output"
	[ -t 1 ] && printf "%b" "$msg"
	[ "$msg" != "${msg//\\n}" ] && {
		[ -s "$queue" ] && msg="$(cat "$queue")${msg}" && rm -f "$queue"
		msg="$(printf "%b" "$msg" | sed 's/\x1b\[[0-9;]*m//g')"
		logger -t "$packageName [$$]" "$(printf "%b" "$msg")"
	} || printf "%b" "$msg" >> "$queue"
}
output_ok() { output "$_OK_"; }
output_okn() { output "${_OK_}\\n"; }
output_fail() { output "$_FAIL_"; }
output_failn() { output "${_FAIL_}\\n"; }
str_contains() { [ "${1//$2}" != "$1" ]; }
str_contains_word() { echo "$1" | grep -qw "$2"; }
uci_add_list_if_new() {
	local PACKAGE="$1"
	local CONFIG="$2"
	local OPTION="$3"
	local VALUE="$4"
	local i
	[ -n "$PACKAGE" ] && [ -n "$CONFIG" ] && [ -n "$OPTION" ] && [ -n "$VALUE" ] || return 1
	for i in $(uci_get "$PACKAGE" "$CONFIG" "$OPTION"); do
		[ "$i" = "$VALUE" ] && return 0
	done
	uci_add_list "$PACKAGE" "$CONFIG" "$OPTION" "$VALUE"
}
uci_changes() {
	local PACKAGE="$1"
	local CONFIG="$2"
	local OPTION="$3"
	[ -s "${UCI_CONFIG_DIR:-/etc/config/}${PACKAGE}" ] && \
	[ -n "$(/sbin/uci ${UCI_CONFIG_DIR:+-c $UCI_CONFIG_DIR} changes "$PACKAGE${CONFIG:+.$CONFIG}${OPTION:+.$OPTION}")" ]
}
version() { echo "$PKG_VERSION"; }

xappend() { PROG_param="$PROG_param $1"; }

append_bool() {
	local section="$1"
	local option="$2"
	local value="$3"
	local default="${4:-0}"
	local _loctmp
	config_get_bool _loctmp "$section" "$option" "$default"
	[ "$_loctmp" -ne 0 ] && xappend "$value"
}

append_parm() {
	local section="$1"
	local option="$2"
	local switch="$3"
	local default="$4"
	local skip_value="$5"
	local _loctmp
	config_get _loctmp "$section" "$option" "$default"
	[ -n "$_loctmp" ] && [ "$_loctmp" != "$skip_value" ] && xappend "$switch $_loctmp"
}

append_cnt() {
	local section="$1"
	local option="$2"
	local switch="$3"
	local default="${4:-0}"
	local _loctmp i
	config_get _loctmp "$section" "$option" "$default"
# shellcheck disable=SC2086,SC2154
	for i in $(seq 1 $_loctmp); do
		xappend '-v'
	done
}

append_boot() {
	local section="$1"
	local option="$2"
	local switch="$3"
	local default="$4"
	local _old_ifs="$IFS"
	local _loctmp _newtmp i
	config_get _loctmp "$section" "$option" "$default"
	[ -z "$_loctmp" ] && return 0
	IFS=" ,"
	for i in $_loctmp; do
		if { [ -z "$force_ipv6" ] && is_ipv4 "$i"; } || \
			{ [ -n "$force_ipv6" ] && is_ipv6 "$i"; }; then
			[ -z "$_newtmp" ] && _newtmp="$i" || _newtmp="${_newtmp},${i}"
		fi
	done
	IFS="$_old_ifs"
	[ -n "$_newtmp" ] && xappend "$switch $_newtmp"
	[ -z "$force_ipv6" ] && xappend '-4'
}

boot() {
	rc_procd start_service 'on_boot' && service_started 'on_boot'
}

load_package_config() {
	local param="$1"
	config_load "$packageName"
	config_load "$packageName"
	config_get_bool canary_domains_icloud  'config' 'canary_domains_icloud' '1'
	config_get_bool canary_domains_mozilla 'config' 'canary_domains_mozilla' '1'
	config_get_bool force_dns              'config' 'force_dns' '1'
	config_get_bool procd_trigger_wan6     'config' 'procd_trigger_wan6' '0'
	config_get_bool global_force_http1     'config' 'force_http1' '0'
	config_get_bool global_force_http3     'config' 'force_http3' '0'
	config_get_bool global_force_ipv6      'config' 'global_force_ipv6_resolvers' '0'
	config_get dnsmasq_config_update       'config' 'dnsmasq_config_update' '*'
	config_get force_dns_port              'config' 'force_dns_port' '53 853'
	config_get force_dns_src_interface     'config' 'force_dns_src_interface' 'lan'
	config_get global_listen_addr          'config' 'listen_addr' '127.0.0.1'
	config_get global_tcp_client_limit     'config' 'tcp_client_limit' '20'
	config_get global_polling_interval     'config' 'polling_interval' '120'
	config_get global_proxy_server         'config' 'proxy_server'
	config_get global_max_idle_time        'config' 'max_idle_time' '118'
	config_get global_conn_loss_time       'config' 'conn_loss_time' '15'
	config_get global_ca_certs_file        'config' 'ca_certs_file'
	config_get global_user                 'config' 'user' 'nobody'
	config_get global_group                'config' 'group' 'nogroup'
	config_get global_verbosity            'config' 'verbosity' '0'
	config_get global_logfile              'config' 'logfile'
	config_get global_statistic_interval   'config' 'statistic_interval' '0'
	config_get global_log_limit            'config' 'log_limit' '0'
	[ "$canary_domains_icloud" = '1' ]  && canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsiCloud}"
	[ "$canary_domains_mozilla" = '1' ] && canaryDomains="${canaryDomains:+$canaryDomains }${canaryDomainsMozilla}"
	[ "$force_dns" = '1' ]              || unset force_dns
	[ "$procd_trigger_wan6" = '1' ]     || unset procd_trigger_wan6
}

start_instance() {
	local cfg="$1" param="$2"
	local PROG_param
	local listen_addr listen_port force_ipv6 p url iface

	config_get url             "$cfg" 'resolver_url'
	config_get listen_addr     "$cfg" 'listen_addr' "$global_listen_addr"
	config_get listen_port     "$cfg" 'listen_port' "$port"
	config_get_bool force_ipv6 "$cfg" 'force_ipv6_resolvers' "$global_force_ipv6"
	[ "$force_ipv6" = '1' ] || unset force_ipv6

	append_parm "$cfg" 'resolver_url'       '-r'
	append_parm "$cfg" 'listen_addr'        '-a' "$global_listen_addr"        '127.0.0.1'
	append_parm "$cfg" 'listen_port'        '-p' "$port"
	append_boot "$cfg" 'bootstrap_dns'      '-b' "$DEFAULT_BOOTSTRAP"
	append_parm "$cfg" 'dscp_codepoint'     '-c'
	append_parm "$cfg" 'tcp_client_limit'   '-T' "$global_tcp_client_limit"   '20'
	append_parm "$cfg" 'polling_interval'   '-i' "$global_polling_interval"   '120'
	append_parm "$cfg" 'proxy_server'       '-t' "$global_proxy_server"
	append_bool "$cfg" 'force_http1'        '-x' "$global_force_http1"
	append_bool "$cfg" 'force_http3'        '-q' "$global_force_http3"
	append_parm "$cfg" 'max_idle_time'      '-m' "$global_max_idle_time"      '118'
	append_parm "$cfg" 'conn_loss_time'     '-L' "$global_conn_loss_time"     '15'
	append_parm "$cfg" 'ca_certs_file'      '-C' "$global_ca_certs_file"
	append_parm "$cfg" 'user'               '-u' "$global_user"
	append_parm "$cfg" 'group'              '-g' "$global_group"
	append_parm "$cfg" 'logfile'            '-l' "$global_logfile"
	append_parm "$cfg" 'statistic_interval' '-s' "$global_statistic_interval" '0'
	append_parm "$cfg" 'log_limit'          '-F' "$global_log_limit"          '0'
	append_cnt  "$cfg" 'verbosity'          '-v' "$global_verbosity"

	if [ "$dnsmasq_config_update" = '*' ]; then
		config_load 'dhcp'
		config_foreach dnsmasq_doh_server 'dnsmasq' 'add' "${listen_addr}" "${listen_port}"
		config_foreach dnsmasq_instance_append_force_dns_port 'dnsmasq'
	elif is_alnum "$dnsmasq_config_update"; then
		for i in $dnsmasq_config_update; do
			dnsmasq_doh_server "@dnsmasq[$i]" 'add' "${listen_addr}" "${listen_port}" || \
				dnsmasq_doh_server "${i}" 'add' "${listen_addr}" "${listen_port}"
			dnsmasq_instance_append_force_dns_port "@dnsmasq[$i]" || \
				dnsmasq_instance_append_force_dns_port "${i}"
		done
	fi

	procd_open_instance
# shellcheck disable=SC2086
	procd_set_param command $PROG $PROG_param
	procd_set_param stderr 1
	procd_set_param stdout 1
	procd_set_param respawn
	procd_open_data
	json_add_object mdns
		procd_add_mdns_service "$packageName" 'udp' "$listen_port" "DNS over HTTPS proxy"
	json_close_object
	if [ -n "$force_dns" ]; then
		json_add_array firewall
		for iface in ${force_dns_src_interface//,/ }; do
			for p in ${force_dns_port//,/ }; do
				if is_port_listening "$p"; then
					json_add_object ''
					json_add_string type 'redirect'
					json_add_string target 'DNAT'
					json_add_string src "$iface"
					json_add_string proto 'tcp udp'
					json_add_string src_dport '53'
					json_add_string dest_port "$p"
					json_add_string family 'any'
					json_add_boolean reflection '0'
					json_close_object
				else
					json_add_object ''
					json_add_string type 'rule'
					json_add_string src "$iface"
					json_add_string dest '*'
					json_add_string proto 'tcp udp'
					json_add_string dest_port "$p"
					json_add_string target 'REJECT'
					json_close_object
				fi
			done
		done
		json_close_array
		unset force_dns
	fi
	procd_close_data
	procd_close_instance

# shellcheck disable=SC2181
	if [ "$?" -eq 0 ]; then
		output_ok
		port="$((port+1))"
	else
		output_fail
	fi
}

start_service() {
	local param="$1"
	local canaryDomains
	local force_dns="$force_dns"
	local port=5053

	[ "$param" = 'on_boot' ] && hdp_boot_flag='true' && return 0

	output "Starting $serviceName instances ${param:+$param }"
	load_package_config "$param"
	dhcp_backup 'create'
	config_load "$packageName"
	config_foreach start_instance "$packageName" "$param"
	output "\\n"
	if uci_changes 'dhcp'; then
		output "Updating dnsmasq config "
		if uci_commit 'dhcp'; then
			output_okn
			param='on_config_update'
		else
			output_failn
		fi
	fi
	case "$param" in
		on_boot|on_config_update|on_hotplug)
			output "Restarting dnsmasq ${param:+$param }"
			if dnsmasq_restart; then
				output_okn
			else
				output_failn
			fi
		;;
	esac
#	if ! is_resolver_working; then
#		rc_procd stop_service 'on_failed_health_check' && service_stopped 'on_failed_health_check'
#	fi
}

stop_service() {
	local param="$1"
	local canaryDomains
	local _error=
	output "Stopping $serviceName ${param:+$param }"
	load_package_config "$param"
	dhcp_backup 'restore'
	if uci_changes 'dhcp'; then
		uci_commit 'dhcp'
		dnsmasq_restart || _error=1
	fi
# shellcheck disable=SC2015
	[ -z "$_error" ] && output_okn || output_failn
}

# shellcheck disable=SC2015
service_triggers() {
	local wan wan6 i
	if [ -n "$hdp_boot_flag" ]; then
		output "Setting trigger (on_boot) "
		procd_add_raw_trigger "interface.*.up" 5000 "/etc/init.d/${packageName}" reload 'on_interface_up' && output_okn || output_failn
	else
		. "${IPKG_INSTROOT}/lib/functions/network.sh"
		network_flush_cache
		network_find_wan wan
		wan="${wan:-wan}"
		if [ -n "$procd_trigger_wan6" ]; then
			network_find_wan6 wan6
			wan6="${wan6:-wan6}"
		fi
		output "Setting trigger${wan6:+s} for $wan ${wan6:+$wan6 }"
		for i in $wan $wan6; do
			procd_add_interface_trigger "interface.*" "$i" "/etc/init.d/${packageName}" reload 'on_interface_trigger' && output_ok || output_fail
		done
		output '\n'
		procd_add_config_trigger "config.change" "$packageName" "/etc/init.d/${packageName}" reload 'on_config_change'
	fi
}

service_started() { [ -n "$force_dns" ] && procd_set_config_changed firewall; }
service_stopped() { [ -n "$force_dns" ] && procd_set_config_changed firewall; }
restart() { reload "$@"; }

dnsmasq_instance_append_force_dns_port() {
	local cfg="$1" instance_port
	[ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1
	config_get instance_port "$cfg" 'port' '53'
		str_contains_word "$force_dns_port" "$instance_port" || force_dns_port="${force_dns_port:+$force_dns_port }${instance_port}"
}

dnsmasq_doh_server() {
	local cfg="$1" param="$2" address="${3:-127.0.0.1}" port="$4" i
	case "$param" in
		add)
			if [ -n "$force_dns" ]; then
				for i in $canaryDomains; do
					uci_add_list_if_new 'dhcp' "$cfg" 'server' "/${i}/"
				done
			fi
			case $address in
				0.0.0.0|::ffff:0.0.0.0) address='127.0.0.1';;
				::) address='::1';;
			esac
			uci_add_list_if_new 'dhcp' "$cfg" 'server' "${address}#${port}"
			uci_add_list_if_new 'dhcp' "$cfg" 'doh_server' "${address}#${port}"
		;;
		remove)
			for i in $(uci_get 'dhcp' "$cfg" 'doh_server'); do
				uci_remove_list 'dhcp' "$cfg" 'server' "$i"
				uci_remove_list 'dhcp' "$cfg" 'doh_server' "$i"
			done
			for i in $canaryDomains; do
				uci_remove_list 'dhcp' "$cfg" 'server' "/${i}/"
				uci_remove_list 'dhcp' "$cfg" 'doh_server' "/${i}/"
			done
		;;
	esac
}

dhcp_backup() {
	_dnsmasq_create_server_backup() {
		local cfg="$1" i
		[ -n "$(uci_get 'dhcp' "$cfg")" ] || return 1
	#	uci_remove 'dhcp' "$cfg" 'doh_server' # this removes outdated doh_server entries, but causes unnecessary dnsmasq restarts
		if [ -z "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" ]; then
			if [ -z "$(uci_get 'dhcp' "$cfg" 'noresolv')" ]; then
				uci_set 'dhcp' "$cfg" 'doh_backup_noresolv' '-1'
			else
				uci_set 'dhcp' "$cfg" 'doh_backup_noresolv' "$(uci_get 'dhcp' "$cfg" noresolv)"
			fi
			uci_set 'dhcp' "$cfg" 'noresolv' 1
		fi
		if [ -z "$(uci_get 'dhcp' "$cfg" 'doh_backup_server')" ]; then
			if [ -z "$(uci_get 'dhcp' "$cfg" 'server')" ]; then
				uci_add_list 'dhcp' "$cfg" 'doh_backup_server' ""
			fi
			for i in $(uci_get 'dhcp' "$cfg" 'server'); do
				uci_add_list 'dhcp' "$cfg" 'doh_backup_server' "$i"
				if [ "$i" = "$(echo "$i" | tr -d /\#)" ]; then
					uci_remove_list 'dhcp' "$cfg" 'server' "$i"
				fi
			done
		fi
		return 0
	}
# shellcheck disable=SC2317
	_dnsmasq_restore_server_backup() {
		local cfg="$1" i
		[ -n "$(uci_get 'dhcp' "$cfg")" ] || return 0
		if [ -n "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" ]; then
			if [ "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')" = "-1" ]; then
				uci_remove 'dhcp' "$cfg" 'noresolv'
			else
				uci_set 'dhcp' "$cfg" 'noresolv' "$(uci_get 'dhcp' "$cfg" 'doh_backup_noresolv')"
			fi
			uci_remove 'dhcp' "$cfg" 'doh_backup_noresolv'
		fi
		if uci_get 'dhcp' "$cfg" 'doh_backup_server' >/dev/null 2>&1; then
			dnsmasq_doh_server "$cfg" 'remove'
			for i in $(uci_get 'dhcp' "$cfg" 'doh_backup_server'); do
				uci_add_list_if_new 'dhcp' "$cfg" 'server' "$i"
			done
			uci_remove 'dhcp' "$cfg" 'doh_backup_server'
		fi
	}
	local i
	config_load 'dhcp'
	case "$1" in
		create)
			if [ "$dnsmasq_config_update" = "*" ]; then
				config_foreach _dnsmasq_create_server_backup 'dnsmasq'
			elif [ -n "$dnsmasq_config_update" ]; then
				for i in $dnsmasq_config_update; do
					if [ -n "$(uci_get 'dhcp' "@dnsmasq[$i]")" ]; then
						_dnsmasq_create_server_backup "@dnsmasq[$i]"
					elif [ -n "$(uci_get 'dhcp' "$i")" ]; then
						_dnsmasq_create_server_backup "$i"
					fi
				done
			fi
			;;
		restore)
			config_foreach _dnsmasq_restore_server_backup 'dnsmasq'
			;;
	esac
}
